Appearance
[中文版本](/posts/c教程/zh-CN/07-贴近系统/070-Practice Mini Shell in C)
Follow-Along Deliverable
- Assumed state: lessons 061-069 are complete and you can rebuild the previous example.
- Working directory:
~/c-course-labs/070-mini-shell. - First command: on macOS / Linux run
mkdir -p ~/c-course-labs/070-mini-shell && cd ~/c-course-labs/070-mini-shell; on Windows PowerShell runNew-Item -ItemType Directory -Force "$HOME\c-course-labs\070-mini-shell"; Set-Location "$HOME\c-course-labs\070-mini-shell". - Success evidence: keep the source file, executable,
evidence.md, and recordpwd,echo, and one pipe or redirection command record. - Boundary for this lab: This lab builds an observable shell prototype. Job control, a full scripting language, and a production sandbox are out of scope.
- Reset: remove the executable, temporary data, and generated output for this lab; keep source and
evidence.mdfor review.
It’s time to bring together everything you’ve learned so far, system calls, process creation, pipes, redirection, and signal handling, into one cohesive project.
In this section, you’ll build a minimal interactive shell, just likebash orzsh, but stripped down to the essentials. It will run commands, handle input/output redirection, and even support pipelines.
Step 1. What You’ll Build
Your mini shell will:
- Display a prompt like
$ - Read user input (e.g.,
ls -l,cat file.txt) - Parse it into command and arguments
- Create a new process using
fork() - Replace the process image with the requested command using
execvp() - Wait for the child to finish
Optional extensions:
- Handle signals (Ctrl+C) gracefully
- Redirect output to a file (
>redirection) - Chain commands using pipes (
|)
Step 2. Core Loop Skeleton
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX 1024
int main(void) {
char input[MAX];
while (1) {
printf("$ ");
fflush(stdout);
if (!fgets(input, sizeof(input), stdin))
break;
// Remove newline
input[strcspn(input, "\n")] = 0;
// Exit command
if (strcmp(input, "exit") == 0)
break;
// Tokenize input
char *args[64];
int i = 0;
char *token = strtok(input, " ");
while (token) {
args[i++] = token;
token = strtok(NULL, " ");
}
args[i] = NULL;
// Fork and execute
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
perror("execvp");
exit(1);
} else if (pid > 0) {
wait(NULL);
} else {
perror("fork");
}
}
printf("Goodbye!\n");
return 0;
}Compile and run:
gcc mini_shell.c -o mini_shell
./mini_shellTry commands:
$ ls
$ pwd
$ echo hello world
$ exitStep 3. Handling Errors Gracefully
If you enter a wrong command:
$ xyzOutput:
execvp: No such file or directoryThis happens because the program handlesexecvp() failure properly withperror(), just as you learned in section 69.
Step 4. Adding Signal Handling
Let’s make Ctrl+C stop the running command, but not kill the shell itself.
#include <signal.h>
void sigint_handler(int sig) {
printf("\nType 'exit' to quit.\n$ ");
fflush(stdout);
}
int main(void) {
signal(SIGINT, sigint_handler);
...
}Now the shell ignores Ctrl+C while waiting for input, instead of terminating.
Step 5. Supporting Output Redirection
We’ll add> redirection like:
$ echo Hello > out.txtAdd this before theexecvp() call in the child:
#include <fcntl.h>
for (int j = 0; args[j]; j++) {
if (strcmp(args[j], ">") == 0) {
args[j] = NULL;
int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
break;
}
}Nowstdout of the command goes to the file instead of the screen.
Step 6. Supporting Input Redirection
Similarly, for< redirection:
$ cat < in.txtAdd:
if (strcmp(args[j], "<") == 0) {
args[j] = NULL;
int fd = open(args[j + 1], O_RDONLY);
dup2(fd, STDIN_FILENO);
close(fd);
break;
}Step 7. Adding Pipe Support
To handle commands like:
$ ls | wc -lWe create two processes connected by a pipe.
int pipefd[2];
pipe(pipefd);
pid_t p1 = fork();
if (p1 == 0) {
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[0]);
execlp("ls", "ls", NULL);
}
pid_t p2 = fork();
if (p2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[1]);
execlp("wc", "wc", "-l", NULL);
}
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);That’s the same pattern you saw in section 64,ls | wc -l.
Step 8. Combining All Features
Your shell now:
- Parses user input
- Spawns child processes
- Handles I/O redirection
- Supports Ctrl+C interruption
- Runs simple pipelines
With ~150 lines of code, you have a working Unix shell prototype.
Step 9. Tiny Code: Full Mini Shell
Here’s the clean, minimal version:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
void sigint_handler(int sig) {
printf("\n$ ");
fflush(stdout);
}
int main(void) {
signal(SIGINT, sigint_handler);
char input[1024];
while (1) {
printf("$ ");
fflush(stdout);
if (!fgets(input, sizeof(input), stdin)) break;
input[strcspn(input, "\n")] = 0;
if (strcmp(input, "exit") == 0) break;
char *args[64];
int i = 0;
char *token = strtok(input, " ");
while (token) {
args[i++] = token;
token = strtok(NULL, " ");
}
args[i] = NULL;
pid_t pid = fork();
if (pid == 0) {
for (int j = 0; args[j]; j++) {
if (strcmp(args[j], ">") == 0) {
int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
args[j] = NULL;
} else if (strcmp(args[j], "<") == 0) {
int fd = open(args[j + 1], O_RDONLY);
dup2(fd, STDIN_FILENO);
close(fd);
args[j] = NULL;
}
}
execvp(args[0], args);
perror("execvp");
exit(1);
} else if (pid > 0) {
wait(NULL);
}
}
printf("Exiting shell.\n");
return 0;
}Try it out, it’s a real, interactive shell.
Step 10. Why It Matters
This exercise combines everything you’ve learned in Chapter 7:
- System calls (
fork,exec,wait,pipe,dup2) - Signals (
SIGINT) - File descriptors and redirection
- Error handling with
errno - Environment inheritance
You’ve just built a simplified version of the core that powers every Unix shell, frombash tozsh tofish.
Try It Yourself
- Add support for pipelines (
|) by chaining multiple commands. - Implement background processes with
&. - Add
cdandpwdas built-in commands. - Display the exit code after each command.
- Handle multiple spaces and quoted arguments.
Next, we’ll move into Chapter 8: Debugging, Testing, and Profiling, starting with gdb, your most powerful ally in understanding and fixing C programs.
